/* sprite-sheet-widget.vala
 *
 * Copyright (C) 2010, 2011  Richard Talbot-Watkins
 * Copyright (C) 2010, 2011  Dave Jeffery
 * 
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Authors:
 *  Richard Talbot-Watkins <richtw1@gmail.com>
 * 	Dave Jeffery <david.richard.jeffery@gmail.com>
 */

using Gtk;
using Gee;
using Cairo;

/**
 * The custom widget used to display sprite sheet contents.
 *
 * @author Richard Talbot-Watkins <richtw1@gmail.com>
 * @author Dave Jeffery <david.richard.jeffery@gmail.com>
 * @version 2011.0110
 * @since 0.1
 */
public class BeebSpriter.SpriteSheetWidget : IconView {

	/**
	 * Sprite sheet widget contains 1 column - the sprite in column 0.
	 */
	private enum Columns {
		SPRITE,
		NUMBER_OF_COLUMNS
	}

	/**
	 * Context menu that appears over empty areas of sprite sheet widget.
	 */
	private Menu empty_menu;

	/**
	 * Empty area New Sprite context menu item.
	 */
	private MenuItem empty_new_sprite ;

	/**
	 * Context menu that appears over cells in sprite sheet widget.
	 */
	private Menu cell_menu;

	/**
	 * Cell Make Copy context menu item.
	 */
	private MenuItem cell_make_copy;

	/**
	 * Cell Delete context menu item.
	 */
	private MenuItem cell_delete;

	/**
	 * Data model for the sprite sheet widget.
	 */
	private ListStore store;

	/**
	 * Default theme background colour for sprite sheet widget.
	 */
	private Gdk.Color default_background_colour;

	/**
	 * Default theme text colour for sprite sheet widget.
	 */
	private Gdk.Color default_text_colour;

	/**
	 * Style for the sprite sheet widget.
	 */
	private Style sprite_sheet_style;

	/**
	 * SpriteSheet instance displayed by this widget.
	 */
	private SpriteSheet sprite_sheet;

	/**
	 * Path of the last cell to have been right-clicked on.
	 */
	private TreePath last_right_clicked;

	/**
	 * Window that creates the sprite sheet widget.
	 */
	private Window parent_window;

	/**
	 * Constructor for the save file dialog box.
	 *
	 * @param sprite_sheet Contains the sprites the widget displays.
	 * @param parent_window Allows dialogs to be transient for the parent window.
	 */ 
	public SpriteSheetWidget (SpriteSheet sprite_sheet, 
								Window parent_window) {
		this.sprite_sheet = sprite_sheet;
		this.parent_window = parent_window;

		store = create_store ();
		fill_store (store);

		store_default_style ();
		set_colours ();
		sprite_sheet.background_changed.connect (set_colours);
		store.row_deleted.connect (on_order_changed);
		store.row_changed.connect (on_order_changed);

		/* Set up the widget's context menus. */
		create_menus ();
		connect_menu_signals ();
		button_press_event.connect (on_button_press_event);
		key_press_event.connect (on_key_press_event);

		model = store;
		selection_mode = SelectionMode.SINGLE;
		orientation = Orientation.VERTICAL;
		columns = -1; // -1 : Fill available area
		item_width = 108; 
		reorderable = true;

		/* Renderer for the sprite bitmap. */
		var bitmap_renderer = new CellRendererPixbuf ();
		pack_start (bitmap_renderer, false); 
		set_cell_data_func (bitmap_renderer, set_sprite_bitmap);

		/* Renderer for the sprite label. */
		var label_renderer = new CellRendererText ();
		label_renderer.editable = true;
		label_renderer.edited.connect (label_edited);
		label_renderer.xalign = 0.5f;
		pack_start (label_renderer, false);
		set_cell_data_func (label_renderer, set_sprite_name);
	}

	/**
	 * Stores the default IconView background and text colours for the current
	 * desktop theme.
	 */
	private void store_default_style () {
		sprite_sheet_style = get_style().clone ();
		default_background_colour = sprite_sheet_style.base[StateType.NORMAL];
		default_text_colour = sprite_sheet_style.text[StateType.NORMAL];
	}

	/**
	 * Called when the sprite order is changed in the sprite sheet widget.
	 */
	private void on_order_changed () {
		/* Get the widget's new sprite order.*/
		Sprite? sprite;
		ArrayList<Sprite> new_order = new ArrayList<Sprite> ();
		TreeIter iter;

		if (store.get_iter_first (out iter)) {
			do {
				store.get (iter, Columns.SPRITE, out sprite);
				new_order.add (sprite);
			} while (store.iter_next (ref iter));
		}

		/* Send the new order to the sprite sheet. */
		sprite_sheet.update_order (new_order);
	}

	/**
	 * Sets the background and text colours for the sprite sheet widget.
	 */
	private void set_colours () {
		sprite_sheet_style = get_style().clone ();

		/* Get the background colour for the sprite sheet. */
		var colour_code = sprite_sheet.background_colour;
		var colour_name = colour_code.to_string ().down ();

		/* Work out background and text colour values. */
		Gdk.Color background;
		Gdk.Color text;
		Gdk.Color.parse (colour_name, out background);
		Gdk.Color.parse ("black", out text);

		/* Deal with special cases. */
		switch (colour_code) {
			/* White text for dark backgrounds. */
			case SpriteSheet.BackColour.BLACK:
			case SpriteSheet.BackColour.BLUE:
				Gdk.Color.parse ("white", out text);
				break;
			/* Restores desktop theme defaults. */
			case SpriteSheet.BackColour.THEME_DEFAULT:
				background = default_background_colour;
				text = default_text_colour;
				break;
		}

		/* Apply the new colours to the widget's style. */
		sprite_sheet_style.base[StateType.NORMAL] = background;
		sprite_sheet_style.text[StateType.NORMAL] = text;
		set_style (sprite_sheet_style);
	}

	/**
	 * Sets a cell's text to the name of the sprite stored in it's model.
	 *
	 * The parameter list is standard for cell renderer data functions.
	 *
	 * @param cell_layout Not used by this function.
	 * @param cell The cell of the IconView whose text we want to set.
	 * @param tree_model The ListStore used as a model for the IconView.
	 * @param iter Points to the data for cell we're setting in the ListStore.
	 */
	private void set_sprite_name (CellLayout cell_layout, CellRenderer cell, 
									TreeModel tree_model, TreeIter iter) {

		Sprite? sprite;
		tree_model.get (iter, Columns.SPRITE, out sprite);

		cell.set ("text", sprite.name);
	}

	/**
	 * Sets a sprite's label to the text entered by the user.
	 *
	 * @param cell_renderer_text The label before it was edited.
	 * @param path_string The path to the cell that was edited.
	 * @param new_text The edited label.
	 */
	private void label_edited (CellRendererText cell_renderer_text, 
								string path_string, string new_text) {
		Sprite? sprite;

		TreeIter iter;
		TreeModel tree_model = get_model ();
		TreePath path = new TreePath.from_string (path_string);
		tree_model.get_iter (out iter, path);

		tree_model.get (iter, Columns.SPRITE, out sprite);
		sprite.name = new_text;
	}

	/**
	 * Sets a cell's image to the sprite's bitmap data.
	 *
	 * The parameter list is standard for cell renderer data functions.
	 *
	 * @param cell_layout Not used by this function.
	 * @param cell The cell of the IconView whose text we want to set.
	 * @param tree_model The ListStore used as a model for the IconView.
	 * @param iter Points to the data for cell we're setting in the ListStore.
	 */
	private void set_sprite_bitmap (CellLayout cell_layout, 
									CellRenderer cell, 
									TreeModel tree_model, TreeIter iter) {

		Sprite? sprite;
		tree_model.get (iter, Columns.SPRITE, out sprite);

		uchar[] data = sprite.get_as_rgba ();

		var pixbuf = new Gdk.Pixbuf.from_data (
												data, 
												Gdk.Colorspace.RGB, 
												true, 
												8, 
												sprite.width, 
												sprite.height, 
												sprite.width * 4, 
												null);

		/* Scale pixbuf so the pixels are the correct aspect ratio. */
		var scaled = pixbuf.scale_simple (
										sprite.width * sprite_sheet.x_scale, 
										sprite.height * sprite_sheet.y_scale, 
										Gdk.InterpType.NEAREST);

		/* Taking copy is necessary to prevent graphical corruption. */
		cell.set ("pixbuf", scaled.copy (), null);
	}

	/**
	 * Creates the ListStore used as the model for the sprite sheet widget.
	 *
	 * @return A ListStore instance set up for sprite sheet data.
	 */
	private ListStore create_store () {
		return new ListStore (Columns.NUMBER_OF_COLUMNS, typeof (Sprite));
	}

	/**
	 * Fills the ListStore used as a model for the widget.
	 *
	 * @param store The ListStore to fill with SpriteSheet Sprite names.
	 */
	private void fill_store (ListStore store) {
		TreeIter iter;
		var sprite_list = sprite_sheet.get_sprite_list ();

		foreach (var sprite in sprite_list) {
			store.append (out iter);
			store.set (iter, Columns.SPRITE, sprite);
		}
	}

	/**
	 * Event sent when the mouse is pressed over the EditorPanel widget.
	 *
	 * @param event Used to work out mouse button and position.
	 * @return false to allow other handlers to receive this signal.
	 */
    private bool on_button_press_event (Gdk.EventButton event) {
		/* Store mouse button and position. */
		var mouse_button = (MouseButton) event.button;
		var mouse_x = (int) event.x;
		var mouse_y = (int) event.y;

		/* We're only interested in the right button. */
		if (mouse_button != MouseButton.RIGHT) { return false; }

		/* Work out where the user clicked. */
		last_right_clicked = get_path_at_pos (mouse_x, mouse_y);
		
		if (last_right_clicked == null) {
			/* Right click was on blank area. */
			empty_menu.popup (null, null, null, event.button, event.time);
		} else {
			/* Show the right click menu. */
			select_path (last_right_clicked);
			cell_menu.popup (null, null, null, event.button, event.time);
		}

		/* Allow other handlers to handle the event too. */
        return false;
    }

	/**
	 * Event sent when the key is pressed over the EditorPanel widget.
	 *
	 * @param event Used to work out the key pressed.
	 * @return false to allow other handlers to receive this signal.
	 */
	private bool on_key_press_event (Gdk.EventKey event) {
		switch (Gdk.keyval_name (event.keyval)) { 
			case "Delete":
			case "KP_Delete":
				on_delete_pressed ();
				break;
			case "Insert":
			case "KP_Insert":
				on_empty_new_sprite ();
				break;
			default:
				stderr.printf(Gdk.keyval_name (event.keyval));
				break;
		}

		/* Allow other handlers to handle the event too. */
        return false;
	}

	/**
	 * Returns the sprite stored in the model for a given cell path.
	 *
	 * @param The path to a cell in the sprite sheet.
	 * @return The sprite object contained in the model for that cell.
	 */
	private Sprite get_sprite_from_path (TreePath tree_path) {
		Sprite sprite;
		TreeIter iter;
		TreeModel tree_model = get_model ();
		tree_model.get_iter (out iter, tree_path);
		tree_model.get (iter, Columns.SPRITE, out sprite);
		return sprite;
	}

	/**
	 * Creates right click menus that are used in the widget.
	 */
	private void create_menus () {
		/* Context menu that appears over empty areas. */
		empty_menu = new Menu ();
		empty_new_sprite = new MenuItem.with_mnemonic (_("New Sprite..."));
		empty_menu.append (empty_new_sprite);
		empty_menu.show_all ();

		/* Context menu that appears over cells. */
		cell_menu = new Menu ();
		cell_make_copy = new MenuItem.with_mnemonic (_("Make copy..."));
		cell_delete = new MenuItem.with_mnemonic (_("Delete"));
		cell_menu.append (cell_make_copy);
		cell_menu.append (cell_delete);
		cell_menu.show_all ();
	}

	/**
	 * Connects the menu bar signal handlers to menu bar signals.
	 */
	private void connect_menu_signals () {
		empty_new_sprite.activate.connect (on_empty_new_sprite);
		cell_make_copy.activate.connect (on_cell_make_copy);
		cell_delete.activate.connect (on_cell_delete);
	}

	/**
	 * Called when New Sprite clicked on sprite sheet context menu.
 	 */
	private void on_empty_new_sprite () {
		/* Work out sprite minimum and maximum width. */
		var min_width = (float) sprite_sheet.pixels_per_byte;
		var max_width = (float) 80 * sprite_sheet.pixels_per_byte;

		var dialog = new NewSpriteDialog (parent_window, min_width, max_width);
		var response = dialog.run ();

		/* Do nothing if the user doesn't create a sprite. */
		if (response != ResponseType.APPLY) {
			dialog.destroy ();
			return;
		}

		/* Create a new sprite. */
		var width = (int) dialog.width_spinbutton.value;
		var height = (int) dialog.height_spinbutton.value;
		var name = dialog.name_entry.text;
		var palette = sprite_sheet.default_palette;
		dialog.destroy ();
		var sprite = new Sprite (name, width, height, palette);

		/* Add new sprite to sprite sheet widget's model. */
		TreeIter iter;
		store.append (out iter);
		store.set (iter, Columns.SPRITE, sprite);
	}

	/**
	 * Called when Make Copy clicked on sprite sheet context menu.
 	 */
	private void on_cell_make_copy () {
		var dialog = new CopySpriteDialog (parent_window);
		var response = dialog.run ();
		var copy_name = dialog.name_entry.text;
		dialog.destroy ();

		/* Do nothing if the user doesn't create a sprite. */
		if (response != ResponseType.APPLY) { return; }

		/* Copy sprite. */
		var clicked_sprite = get_sprite_from_path (last_right_clicked);
		var copied_sprite = new Sprite.from_copy (copy_name, clicked_sprite);

		/* Add copied sprite to sprite sheet widget's model. */
		TreeIter iter;
		TreeModel tree_model = get_model ();
		tree_model.get_iter (out iter, last_right_clicked);
		store.insert_after (out iter, iter);
		store.set (iter, Columns.SPRITE, copied_sprite);
	}

	/**
	 * Called when Delete clicked on sprite sheet context menu.
 	 */
	private void on_cell_delete () {
		delete_sprite (last_right_clicked);
	}

	/**
	 * Called when Delete or KeyPad Delete pressed.
 	 */
	private void on_delete_pressed () {
		var selection = get_selected_items ();
		var path = selection.nth_data (0);
		if (path != null) {
			delete_sprite (path);
		}
	}

	/**
	 * Delete sprite confirmation dialog box.
	 * 
	 * @param message dialog message (pango formatted)
	 * @param secondary_text secondary text (pango formatted)
	 */
	public int delete_confirmation_dialog (string message, 
													string secondary_text) {
		var alert = new MessageDialog.with_markup (parent_window, 
									DialogFlags.MODAL | 
									DialogFlags.DESTROY_WITH_PARENT,
									MessageType.WARNING, 
									ButtonsType.NONE,
									message);
		alert.secondary_text = secondary_text;
		alert.add_button (_("Delete Sprite"), ResponseType.APPLY);
		alert.add_button (STOCK_CANCEL, ResponseType.CANCEL);
		alert.set_default_response (ResponseType.CANCEL);
		var response = alert.run ();
		alert.destroy ();
		return response;
	}

	/**
	 * Deletes a sprite from the IconView's model.
	 *
	 * @param sprite_path TreePath to sprite in TreeModel.
 	 */
	private void delete_sprite (TreePath sprite_path) {
		/* Substitute special entities to allow Pango rendering. */
		var clicked_sprite = get_sprite_from_path (sprite_path);
		var escaped_name =  clicked_sprite.name.replace ("&", "&amp;")
												.replace ("<", "&lt;")
												.replace (">;", "&gt;");
		/* This rather odd syntax is necessary for i18n. */
		var message = 
		_("<big><b>Are you sure you want to delete the sprite \"%s\"?</b></big>")
		.printf (escaped_name);
		var secondary_text = 
		_("This sprite will be lost forever if you delete it.");
		var response = delete_confirmation_dialog (message, secondary_text);
		if (response != ResponseType.APPLY) {
			return;
		}

		/* Delete sprite from sprite sheet widget's model. */
		TreeIter iter;
		TreeModel tree_model = get_model ();
		tree_model.get_iter (out iter, sprite_path);
		store.remove (iter);
	}
}
